package com.nf.mvc.argument;

import com.nf.mvc.MethodArgumentResolver;
import com.nf.mvc.handler.HandlerMethod;
import com.nf.mvc.support.Constants;
import com.nf.mvc.util.AnnotationUtils;
import com.nf.mvc.util.ClassUtils;
import com.nf.mvc.util.ReflectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.util.Objects;

/**
 * 此类是某个方法的某一个参数的封装类，里面主要封装了
 * <ul>
 *     <li>{@link Parameter}</li>
 *     <li>{@link Method}</li>
 *     <li>参数名:{@link #getParameter()}</li>
 *     <li>参数在方法中的位置，以0开头:{@link #getIndex()} ()}</li>
 *     <li>参数所在方法所在的类:{@link #getContainingClass()} ()} ()}</li>
 * </ul>
 * <h3>关于参数名</h3>
 * <p>优先以RequestParam注解指定的值为准，如果没有指定，就以反射获取的名字为准，方法{@link #getName()}实现了这个逻辑。
 * 所以，所有的方法参数解析器只要通过{@link #getName()}来获取参数名，那么它就支持通过注解指定参数名方式</p>
 *
 * <h3>hashCode与equals的重写</h3>
 * <p>这个类之所以重写这些内容是因为参数类型对应的解析器我进行了缓存的处理，而缓存又是以此类型作为key值的</p>
 *
 * <h3>isList与isArray</h3>
 * <p>关于这两个方法及其相关的方法放在这里的原因是，本mvc框架对参数进行解析的时候，
 * 主要只支持T,T[],List<T>这三种情况，放在这里便于解析器调用，也不存在非常不合理的逻辑</p>
 *
 * <h3>为什么不持有HandlerMethod，而是持有Method</h3>
 * <p>HandlerMethod包含Method也有一些便利的API，比如{@link HandlerMethod#getParameterCount()}之类的,
 * 主要的原因是Method的通用性更强，HandlerMethod毕竟是mvc框架内的一个类型，所以并不适合在出现Method的地方，全部用HandlerMethod取代。
 * 比如ReflectionUtils的getParameterNames方法与MethodInvoker的invoke这些比较通用型的类型，是不应该使用HandlerMethod这个独属于Mvc框架的类的。
 * 鉴于这些通用型的类型也用到了MethodParameter，所以为了协调，这里也没有用HandlerMethod。
 * </p>
 *
 * @see com.nf.mvc.MethodArgumentResolver
 * @see com.nf.mvc.support.MethodInvoker
 * @see HandlerMethod
 * @see RequestParam
 */
public class MethodParameter {
    private final Method method;
    /**
     * 本类不要直接使用此变量，要通过getParameter方法来使用parameter变量，
     * 直接使用此字段会报空引用异常，因为构造函数并没有初始化这个变量。
     * 此字段的赋值是依靠参数在方法里的位置（parameterIndex）来实现的，
     * 而parameterIndex主要用在此类的{@link #equals(Object)}与{@link #hashCode()}实现里
     */
    private Parameter parameter;
    private final String parameterName;
    private final int parameterIndex;
    private final Class<?> containingClass;

    public MethodParameter(Method method, int parameterIndex, String parameterName) {
        this(method, parameterIndex, parameterName, method.getDeclaringClass());
    }

    public MethodParameter(Method method, int parameterIndex, String parameterName, Class<?> containingClass) {
        this.method = method;
        this.parameterIndex = parameterIndex;
        this.parameterName = parameterName;
        this.containingClass = containingClass;
    }

    public Parameter getParameter() {
        if (this.parameterIndex < 0) {
            throw new IllegalStateException("无效的参数索引");
        }
        Parameter parameter = this.parameter;
        if (parameter == null) {
            parameter = method.getParameters()[this.parameterIndex];
            this.parameter = parameter;
        }
        return parameter;
    }

    public Class<?> getType() {
        return getParameter().getType();
    }

    /**
     * 获取参数的名字，如果方法参数有注解RequestParam修饰，并指定了名字就以注解指定的为准，
     * 否则，靠反射解析方法参数名，这就需要在编译的时候添加-parameters选项
     *
     * <p>方法参数上有注解，并且name属性的值不等于DEFAULT_NONE时才是真正通过注解设置了参数名,
     * 因为用户可能只是利用RequestParam设置了默认值,没有设置参数名。默认值的处理逻辑也是类似的</p>
     *
     * @return 返回参数名
     */
    public String getName() {
        if (!isPresent(RequestParam.class)) {
            return this.parameterName;
        }
        return AnnotationUtils.getAttrValue(getParameter(), RequestParam.class, "name", Constants.DEFAULT_NONE, this.parameterName);

    }

    public Object getDefaultValue() {
        if (!isPresent(RequestParam.class)) {
            // 从请求端获取的值为null才去查看注解设定的默认值，这里没有注解返回null值相当于保留获取不到数据的情况
            return null;
        }
        return AnnotationUtils.getAttrValue(getParameter(), RequestParam.class, "defaultValue", Constants.DEFAULT_NONE, null);
    }


    public <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
        return getParameter().getDeclaredAnnotation(annotationClass);
    }

    public Method getMethod() {
        return method;
    }

    public int getIndex() {
        return parameterIndex;
    }

    public Class<?> getContainingClass() {
        return containingClass;
    }

    public Class<?> getComponentType() {
        if (!isArray()) {
            throw new IllegalStateException("不是数组，无法获取数组的成员类型");
        }
        return getType().getComponentType();
    }

    /**
     * 这个方法主要是为了获取List<String>这样的方法参数的实参String这样的类型
     *
     * @return 返回第一个泛型实参类型
     */
    public Class<?> getFirstActualArgument() {
        return getActualArguments()[0];
    }

    /**
     * 此方法是用来获取方法泛型参数的类型实参的。
     *
     * @return 返回所有的泛型实参类型数组
     */
    public Class<?>[] getActualArguments() {
        return ReflectionUtils.getActualArguments(getParameter());
    }

    public boolean isArray() {
        return getType().isArray();
    }

    public boolean isList() {
        return ClassUtils.isList(getType());
    }

    public boolean isSimpleType() {
        return ClassUtils.isSimpleType(getType());
    }

    /**
     * 判断方法的参数是不是一个参数化的类型，比如List<String>这种写法就是一个参数化类型，也就是一个泛型实参化的类型
     *
     * @return true表示是一个泛型实参化的类型，否则是false
     */
    public boolean isParameterizedType() {
        return getParameter().getParameterizedType() instanceof ParameterizedType;
    }

    public boolean isPresent(Class<? extends Annotation> annotationClass) {
        return getParameter().isAnnotationPresent(annotationClass);
    }


    /**
     * 为了避免每次对方法参数{@link MethodParameter}进行解析的时候去查找其对应的参数解析器{@link MethodArgumentResolver}，
     * 所以需要把这种匹配关系缓存起来，通常是用一个HashMap结构来缓存，键是{@link MethodParameter},值是{@link MethodArgumentResolver}，
     * 所以需要重写{@link Object#equals(Object)}与{@link Object#hashCode()},以便进行更合理的hash比较与判等比较
     *
     * <h3>在缓存中的应用</h3>
     * <p>具体的实现见{@link MethodArgumentResolverComposite#getArgumentResolver(MethodParameter)}</p>
     *
     * <h3>hashcode与equals为什么要重写</h3>
     * <p>当我们对比两个对象是否相等时，我们就可以先使用 hashCode 进行比较，如果比较的结果是 true，那么就可以使用 equals 再次确认两个对象是否相等，
     * 如果比较的结果是 true，那么这两个对象就是相等的，否则其他情况就认为两个对象不相等</p>
     * <p>这样就大大的提升了对象比较的效率，这也是为什么 Java 设计使用 hashCode 和 equals 协同的方式，来确认两个对象是否相等的原因</p>
     * <p>那为什么不直接使用 hashCode 就确定两个对象是否相等呢？这是因为不同对象的 hashCode 可能相同；但 hashCode 不同的对象一定不相等，
     * 所以<b><i>使用hashCode 可以起到快速<u>初次判断</u>对象是否相等的作用</i></b>,具体看https://cloud.tencent.com/developer/article/1910930</p>
     *
     * <h3>hashcode与equals方法的重写实现</h3>
     * <p>hashCode与equals方法的实现可以使用{@link Objects#equals(Object, Object)}与{@link Objects#hashCode(Object)},但这里没有采用
     * 可以去网上搜索effective java一书中对这两个方法推荐的实现技巧，
     * 可以阅读<a href="https://stackoverflow.com/questions/2265503/why-do-i-need-to-override-the-equals-and-hashcode-methods-in-java">在这篇文章里有说明为什么要重写</a>.
     * <a href="https://mkyong.com/java/java-how-to-overrides-equals-and-hashcode/">在这篇文章里有教你如何重写</a>
     * <a href="https://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier">这里说明为什么要乘以31</a></p>
     *
     * @param other 其它的要比较的对象
     * @return 相等返回true，否则返回false
     */
    @Override
    public boolean equals(Object other) {
        // 1.自己与其它是同一个（==）对象，那么肯定相等
        if (this == other) {
            return true;
        }
        // 2.别人跟我根本不是同一个类型。所以就一定不相等
        if (!(other instanceof MethodParameter)) {
            return false;
        }
        // 3.代码到这里，就意味着绝不是同一个对象，但类型是一样的，所以可以安全的进行类型转换
        MethodParameter otherParam = (MethodParameter) other;
        // 方法参数所在类一样，所在方法一样，位置一样，那么就可以认为两个对象代表同一个方法参数
        // 当前框架主要是基于类型进行解析的，这个相等实现可以实现为参数类型一样就认为是相等的，但当前没有这样做
        // 主要是想让学生知道常规的equals实现的基本套路
        return (getContainingClass() == otherParam.getContainingClass() &&
                this.parameterIndex == otherParam.parameterIndex &&
                this.getMethod().equals(otherParam.getMethod()));
    }

    @Override
    public int hashCode() {
        return (31 * this.getMethod().hashCode() + this.parameterIndex);
    }
}
